package aole.visq.draw;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.Timer;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoManager;

import jpen.PenManager;
import aole.util.AbstractFile;
import aole.util.Clipboard;
import aole.util.ColorButton;
import aole.util.DialogCreator;
import aole.util.Util;
import aole.visq.draw.undo.UndoableDraw;
import aole.visq.draw.undo.UndoableKey;

public class DrawApp implements ActionListener {
	public static final String	VERSION_MAJOR	= "1";
	public static final String	VERSION_MINOR	= "0";
	public static final String	VERSION_BUILD	= "0 Alpha";

	public enum DRAWMODE {
		BRUSH, ERASER
	}

	private Graphics2D			g2;
	private DrawCanvas			canvas;
	private DrawAnimation		draw;
	private VisqFrame			mFrame;
	private boolean				bdown;
	private int					lx, ly;
	private float				penWidth		= 2f;
	private float				eraserWidth		= 10f;
	private DRAWMODE			drawMode;
	private DrawFile			file;
	private final String		appName			= "Visq Draw";
	private UndoManager			undoManager;
	private BufferedImage		lastImg;
	private boolean				gotAction;
	private BufferedImage		currentImg;
	private boolean				bSkinPrevious;
	private boolean				bSkinNext;
	private Color				strokeColor;
	private Timer				animationTimer;
	// smooth line
	private ArrayList< PointD >	points;
	private boolean				lineSmoothing;
	private int					smoothingDegree	= 2;

	private class DrawFile extends AbstractFile {
		private DialogCreator	newFileDialog;
		private JTextField		txtWidth	= new JTextField( "800", 5 );
		private JTextField		txtHeight	= new JTextField( "600", 5 );
		private ColorButton		btnColor	= new ColorButton( Color.white );
		private JTextField		txtFrames	= new JTextField( "15", 5 );
		private JTextField		txtFPS		= new JTextField( "15", 5 );

		public DrawFile( String applicationTitle ) {
			super( applicationTitle );
			newFileDialog = new DialogCreator( mFrame, "Create New File" );
			newFileDialog.addField( "Document Size", txtWidth, txtHeight );
			newFileDialog.addField( "Background Color", btnColor );
			newFileDialog.addField( "Animation Length", txtFrames );
			newFileDialog.addField( "Frames/Second", txtFPS );
			newFileDialog.pack();
		}

		@Override
		protected boolean doOpen( File f ) {
			try {
				FileInputStream fileIn;
				fileIn = new FileInputStream( f );
				ObjectInputStream in = new ObjectInputStream( fileIn );
				draw = (DrawAnimation) in.readObject();
				redoUI();
				in.close();
				fileIn.close();
				return true;
			} catch( ClassNotFoundException | IOException e1 ) {
				e1.printStackTrace();
			}
			return false;
		}

		@Override
		protected boolean doSave( File f ) {
			try {
				FileOutputStream fileOut = new FileOutputStream( f );
				ObjectOutputStream out = new ObjectOutputStream( fileOut );
				out.writeObject( draw );
				out.close();
				return true;
			} catch( IOException e1 ) {
				e1.printStackTrace();
			}
			return false;
		}

		@Override
		protected boolean doNew() {
			newDocument( 800, 600, Color.white, 15, 15 );
			return true;
		}

		@Override
		protected boolean doNewDialog() {
			newFileDialog.setVisible( true );
			if( newFileDialog.getSelectedOption() == DialogCreator.OK_OPTION ) {
				int w = Integer.parseInt( txtWidth.getText() );
				int h = Integer.parseInt( txtHeight.getText() );
				int f = Integer.parseInt( txtFrames.getText() );
				int fps = Integer.parseInt( txtFPS.getText() );
				newDocument( w, h, btnColor.getColor(), f, fps );
				return true;
			}
			return false;
		}

	}

	private void newDocument( int w, int h, Color bg, int f, int fps ) {
		draw = new DrawAnimation( w, h, f );
		draw.setFPS( fps );
		draw.setBackgroudColor( bg );
		redoUI();
	}

	private void parellelLines() {
		ArrayList< PointD > pts1 = new ArrayList<>();
		ArrayList< PointD > pts2 = new ArrayList<>();

		int n = points.size();
		double x1 = points.get( 0 ).x;
		double y1 = points.get( 0 ).y;
		float p1 = points.get( 0 ).pressure;

		double x2, y2, x, y, vnx = 0, vny = 0;
		double sqr;
		double lw = penWidth / 2.0;

		int pointsToSkip = 0;
		if( lineSmoothing ) pointsToSkip = smoothingDegree;

		for( int i = 1; i < n; i += (pointsToSkip + 1) ) {
			x2 = points.get( i ).x;
			y2 = points.get( i ).y;
			float p2 = points.get( i ).pressure;
			// direction
			x = x2 - x1;
			y = y2 - y1;
			sqr = Math.sqrt( x * x + y * y );
			// unit vector in perpendicular direction
			vnx = y / sqr;
			vny = -x / sqr;
			// multiply the perp. vector with line width/2
			vnx *= lw;
			vny *= lw;
			// add points in both directions to give it proper width
			pts1.add( new PointD( (x1 + vnx * p1), (y1 + vny * p1) ) );
			pts2.add( new PointD( (x1 - vnx * p1), (y1 - vny * p1) ) );
			x1 = x2;
			y1 = y2;
			p1 = p2;
		}
		pts1.add( new PointD( (x1 + vnx * p1), (y1 + vny * p1) ) );
		pts2.add( new PointD( (x1 - vnx * p1), (y1 - vny * p1) ) );

		points.clear();
		points.addAll( pts1 );
		// reverse the second set of points to get proper bezier path
		Collections.reverse( pts2 );
		points.addAll( pts2 );
		// close the path
		points.add( pts1.get( 0 ) );

		if( lineSmoothing )
			// create bezier path
			bezierSpline();
		else simpleLine();
	}

	private void simpleLine() {
		int s = points.size();
		if( s < 2 ) return;

		if( s < 4 ) {
			// TODO: draw circle
			return;
		}

		Path2D.Double curve = new Path2D.Double();

		PointD p1 = points.get( s - 1 );
		curve.moveTo( p1.x, p1.y );
		for( PointD p2: points ) {
			curve.lineTo( p2.x, p2.y );
		}

		g2.fill( curve );
		points.clear();
	}

	// http://www.codeproject.com/Articles/31859/Draw-a-Smooth-Curve-through-a-Set-of-2D-Points-wit
	private void bezierSpline() {
		int n = points.size() - 1;
		if( n < 2 ) return;

		Path2D.Double curve = new Path2D.Double();
		// Calculate first Bezier control points
		// Right hand side vector
		double rhs[] = new double[n];
		// Set right hand side X values
		for( int i = 1; i < n - 1; i++ )
			rhs[i] = 4 * points.get( i ).x + 2 * points.get( i + 1 ).x;
		rhs[0] = points.get( 0 ).x + 2 * points.get( 1 ).x;
		rhs[n - 1] = (8 * points.get( n - 1 ).x + points.get( n ).x) / 2.0;
		// Get first control points X-values
		double[] x = GetFirstControlPoints( rhs );

		// Set right hand side Y values
		for( int i = 1; i < n - 1; ++i )
			rhs[i] = 4 * points.get( i ).y + 2 * points.get( i + 1 ).y;
		rhs[0] = points.get( 0 ).y + 2 * points.get( 1 ).y;
		rhs[n - 1] = (8 * points.get( n - 1 ).y + points.get( n ).y) / 2.0;
		// Get first control points Y-values
		double[] y = GetFirstControlPoints( rhs );

		double x2, y2;
		curve.moveTo( points.get( 0 ).x, points.get( 0 ).y );
		for( int i = 0; i < n; ++i ) {
			// Second control point
			if( i < n - 1 ) {
				x2 = 2 * points.get( i + 1 ).x - x[i + 1];
				y2 = 2 * points.get( i + 1 ).y - y[i + 1];
			} else {
				x2 = (points.get( n ).x + x[n - 1]) / 2;
				y2 = (points.get( n ).y + y[n - 1]) / 2;
			}

			curve.curveTo( x[i], y[i], // first control point
					x2, y2, // second control point
					points.get( i + 1 ).x, points.get( i + 1 ).y );
		}

		g2.fill( curve );
		points.clear();
	}

	private double[] GetFirstControlPoints( double[] rhs ) {
		int n = rhs.length;
		double[] x = new double[n]; // Solution vector.
		double[] tmp = new double[n]; // Temp workspace.

		double b = 2.0;
		x[0] = rhs[0] / b;
		for( int i = 1; i < n; i++ ) // Decomposition and forward substitution.
		{
			tmp[i] = 1 / b;
			b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
			x[i] = (rhs[i] - x[i - 1]) / b;
		}
		for( int i = 1; i < n; i++ )
			x[n - i - 1] -= tmp[n - i] * x[n - i]; // Backsubstitution.

		return x;
	}

	public void setBrush() {
		drawMode = DRAWMODE.BRUSH;
		canvas.setCursorSize( penWidth );
		mFrame.setBrush();
	}

	public void setEraser() {
		drawMode = DRAWMODE.ERASER;
		canvas.setCursorSize( eraserWidth );
		mFrame.setEraser();
	}

	public void setCurrentFrame( int frame ) {
		draw.setCurrentFrame( frame );
		mFrame.setCurrentFrame( frame );
		canvas.repaint();
	}

	public void createKey() {
		draw.createKey();
		canvas.repaint();
		mFrame.setCurrentFrame( draw.getCurrentFrame() );
	}

	public SerializedBI removeKey() {
		SerializedBI img = draw.removeKey();
		canvas.repaint();
		mFrame.setCurrentFrame( draw.getCurrentFrame() );
		return img;
	}

	public void drawImage( BufferedImage img ) {
		draw.drawImage( img );
		canvas.repaint();
	}

	public DrawApp() {
		strokeColor = Color.black;
		lineSmoothing = true;

		// undo manager
		undoManager = new UndoManager();
		undoManager.setLimit( 10 );

		// canvas
		canvas = new DrawCanvas( this );
		canvas.setCursorSize( penWidth );
		// line points array list
		points = new ArrayList<>();

		// animation
		ActionListener animationListener = new ActionListener() {
			@Override
			public void actionPerformed( ActionEvent e ) {
				int nf = draw.getCurrentFrame() + 1;
				if( nf > draw.getTotalFrames() ) nf = 1;
				setCurrentFrame( nf );
			}
		};
		animationTimer = new Timer( -1, animationListener );

		// JPen
		PenManager pm = new PenManager( canvas );
		pm.pen.addListener( new ProcessPen( this ) );

		// application frame
		mFrame = new VisqFrame( this );

		file = new DrawFile( appName );
		file.newFile();

		mFrame.createAndShowGui();
		mFrame.setTitle( file.getWindowTitle() );
		setBrush();
	}

	private void redoUI() {
		int sec = 1000 / draw.getFPS();
		animationTimer.setDelay( sec );

		mFrame.setAnimationWidth( draw.getTotalFrames() );
		mFrame.setCurrentFrame( draw.getCurrentFrame() );

		undoManager.discardAllEdits();
		currentImg = Util.copyImage( draw.getImage() );

		canvas.setSize( draw.getWidth(), draw.getHeight() );
		canvas.setBackground( draw.getBackgroudColor() );

		mFrame.pack();
	}

	public void inputDown( int x, int y, int button ) {
		if( button == 1 ) {
			bdown = true;
			lx = x;
			ly = y;
			g2 = draw.getGraphics();
			if( g2 != null ) {
				if( drawMode == DRAWMODE.BRUSH ) {
					g2.setColor( strokeColor );
					g2.setComposite( AlphaComposite.SrcOver );
					canvas.setStrokeColor( strokeColor );
				} else if( drawMode == DRAWMODE.ERASER ) {
					BasicStroke bs = new BasicStroke( eraserWidth,
							BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
					g2.setStroke( bs );
					g2.setComposite( AlphaComposite.Clear );
				}
			}
		}
	}

	public void inputUp( int x, int y ) {
		bdown = false;

		canvas.clear();
		if( gotAction ) {
			if( drawMode == DRAWMODE.BRUSH ) parellelLines();

			addImageToUndo();
			gotAction = false;
		}
		canvas.repaint();
	}

	public void inputMove( int x, int y, float p ) {
		if( bdown && g2 != null ) {
			if( drawMode == DRAWMODE.BRUSH ) {
				points.add( new PointD( x, y, p ) );

				BasicStroke bs = new BasicStroke( penWidth * p,
						BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND );
				canvas.setStroke( bs );
				canvas.drawLine( lx, ly, x, y );
				canvas.repaint();
				file.setDirty();
				mFrame.setTitle( file.getWindowTitle() );
				gotAction = true;
			} else if( drawMode == DRAWMODE.ERASER ) {
				g2.drawLine( lx, ly, x, y );
				canvas.repaint();
				file.setDirty();
				mFrame.setTitle( file.getWindowTitle() );
				gotAction = true;
			}

		}
		lx = x;
		ly = y;
	}

	public static void main( String[] args ) {
		new DrawApp();
	}

	public BufferedImage getImage() {
		return draw.getImage();
	}

	public Component getCanvas() {
		return canvas;
	}

	public void fileNew() {
		file.newFileDialog();
		mFrame.setTitle( file.getWindowTitle() );
	}

	public void fileSave() {
		file.resetFileFilters();
		file.setFileFilter( file.addFileFilter( "Visq Draw", "aole" ) );
		file.save( mFrame );
		mFrame.setTitle( file.getWindowTitle() );
	}

	public void fileOpen() {
		file.resetFileFilters();
		file.setFileFilter( file.addFileFilter( "Visq Draw", "aole" ) );
		file.open( mFrame );
		mFrame.setTitle( file.getWindowTitle() );
	}

	public void keyRemove() {
		BufferedImage img = removeKey().getImage();
		UndoableKey ukr = new UndoableKey( this, draw.getCurrentFrame(), false,
				img );
		undoManager.addEdit( ukr );
	}

	public void keySet() {
		createKey();
		UndoableKey uka = new UndoableKey( this, draw.getCurrentFrame(), true );
		undoManager.addEdit( uka );
	}

	public void keyAddNextFrame() {
		int cf = draw.getCurrentFrame();
		if( cf == draw.getTotalFrames() ) setTotalFrames( cf + 1 );
		setCurrentFrame( cf + 1 );
		keySet();
	}

	@Override
	public void actionPerformed( ActionEvent e ) {
		switch( e.getActionCommand() ) {
		case "file_new":
			fileNew();
			break;
		case "file_load":
			fileOpen();
			break;
		case "file_save":
			fileSave();
			break;
		case "file_save_as":
			file.resetFileFilters();
			file.setFileFilter( file.addFileFilter( "Visq Draw", "aole" ) );
			file.saveAs( mFrame );
			mFrame.setTitle( file.getWindowTitle() );
			break;
		case "key_set":
			keySet();
			break;
		case "key_remove":
			keyRemove();
			break;
		case "key_add_next":
			keyAddNextFrame();
			break;
		case "undo":
			undo();
			break;
		case "redo":
			redo();
			break;
		case "brush":
			setBrush();
			break;
		case "eraser":
			setEraser();
			break;
		case "animation_play":
			if( ((JToggleButton) e.getSource()).isSelected() )
				animationPlay();
			else animationPause();
			break;
		case "export_movie":
			File f = AbstractFile.getSaveFile( mFrame, "MP4 files", "mp4" );
			if( f != null ) exportMP4( f );
			break;
		case "export_images":
			File d = AbstractFile.getSaveDirectory( mFrame );
			if( d != null ) exportImages( d );
			break;
		case "exit":
			systemExit();
			break;
		case "clear":
			clear();
			break;
		case "copy":
			copy();
			break;
		case "paste":
			paste();
			break;
		}
	}

	private void clear() {
		g2 = draw.getGraphics();
		if( g2 == null ) return;
		g2.clearRect( 0, 0, getWidth(), getHeight() );
		canvas.repaint();
		addImageToUndo();
	}

	public void setSkinPrevious( boolean v ) {
		bSkinPrevious = v;
		mFrame.setSkinPrevious( v );
		canvas.repaint();
	}

	public void setSkinNext( boolean v ) {
		bSkinNext = v;
		mFrame.setSkinNext( v );
		canvas.repaint();
	}

	public void systemExit() {
		if( file.isDirty() ) {
			int res = JOptionPane.showConfirmDialog(
					mFrame,
					"Would you like to save the changes to "
							+ file.getFileName() + "?", "File not saved",
					JOptionPane.YES_NO_CANCEL_OPTION );
			if( res == JOptionPane.CANCEL_OPTION ) return;
			if( res == JOptionPane.YES_OPTION )
				if( !file.save( mFrame ) ) return;
		}
		System.exit( 0 );
	}

	private void animationPause() {
		animationTimer.stop();
	}

	private void animationPlay() {
		animationTimer.start();
	}

	private void addImageToUndo() {
		lastImg = currentImg;
		currentImg = Util.copyImage( draw.getImage() );
		UndoableDraw ud = new UndoableDraw( this, draw.getCurrentFrame(),
				lastImg, currentImg );
		undoManager.addEdit( ud );
	}

	public void redo() {
		try {
			undoManager.redo();
		} catch( CannotRedoException cre ) {}
	}

	public void undo() {
		try {
			undoManager.undo();
			currentImg = Util.copyImage( draw.getImage() );
		} catch( CannotUndoException cre ) {}
	}

	public Dimension getSize() {
		return new Dimension( draw.getWidth(), draw.getHeight() );
	}

	public HashMap< Integer, SerializedBI > getKeys() {
		return draw.getKeys();
	}

	public void currentFrameChanged( int f ) {
		if( f < 1 ) f = 1;
		if( f > draw.getTotalFrames() ) f = draw.getTotalFrames();
		setCurrentFrame( f );
	}

	public static Icon getIcon( String path ) {
		return new ImageIcon( DrawApp.class.getResource( path ) );
	}

	public boolean isSkinPrevious() {
		return bSkinPrevious;
	}

	public boolean isSkinNext() {
		return bSkinNext;
	}

	public BufferedImage getPreviousImage() {
		return draw.getPreviousImage();
	}

	public BufferedImage getNextImage() {
		return draw.getNextImage();
	}

	public Color getStrokeColor() {
		return strokeColor;
	}

	public void setStrokeColor( Color color ) {
		strokeColor = color;
	}

	public void increaseDrawSize() {
		if( drawMode == DRAWMODE.BRUSH ) {
			setBrushWidth( penWidth + 1f );
		} else if( drawMode == DRAWMODE.ERASER ) {
			setEraserWidth( eraserWidth + 1f );
		}
	}

	public void decreaseDrawSize() {
		if( drawMode == DRAWMODE.BRUSH ) {
			setBrushWidth( penWidth - 1f );
		} else if( drawMode == DRAWMODE.ERASER ) {
			setEraserWidth( eraserWidth - 1f );
		}
	}

	public int getWidth() {
		return draw.getWidth();
	}

	public int getHeight() {
		return draw.getHeight();
	}

	// takes directory as input
	public void exportImages( File d ) {
		if( !d.isDirectory() ) return;

		try {
			for( int i = 1; i <= draw.getTotalFrames(); i++ ) {
				String t = String.format( "Image%03d.png", i );
				File f = new File( d, t );
				setCurrentFrame( i );
				ImageIO.write( draw.getFinalImage(), "PNG", f );
			}
		} catch( IOException e ) {
			// TODO Exception message
			e.printStackTrace();
		}
	}

	public void exportMP4( File f ) {
		try {
			SequenceEncoder encoder = new SequenceEncoder( f, draw.getFPS() );
			for( int i = 1; i <= draw.getTotalFrames(); i++ ) {
				setCurrentFrame( i );
				encoder.encodeImage( draw.getFinalImage() );
			}
			encoder.finish();
		} catch( IOException e ) {
			// TODO: Exception message
			e.printStackTrace();
		}
	}

	public void copy() {
		Clipboard.saveImage( draw.getImage() );
	}

	public void paste() {
		Image i = Clipboard.readImage();
		if( i != null ) draw.setImage( i );
		canvas.repaint();
		addImageToUndo();
	}

	public int getFPS() {
		return draw.getFPS();
	}

	public int getTotalFrames() {
		return draw.getTotalFrames();
	}

	public float getBrushWidth() {
		return penWidth;
	}

	public float getEraserWidth() {
		return eraserWidth;
	}

	public Color getBackgroundColor() {
		return draw.getBackgroudColor();
	}

	public boolean isSmoothingOn() {
		return lineSmoothing;
	}

	public int getSmoothingDegree() {
		return smoothingDegree;
	}

	public void setFPS( int value ) {
		draw.setFPS( value );
	}

	public void setTotalFrames( int value ) {
		if( draw.getTotalFrames() > value ) {
			if( JOptionPane
					.showConfirmDialog(
							mFrame,
							"Do you want to reduce the animation length?\nYou might loose set keys!",
							"Reduce animation Length",
							JOptionPane.OK_CANCEL_OPTION ) == JOptionPane.CANCEL_OPTION )
				return;
		}
		if( value < 1 ) value = 1;
		draw.setTotalFrames( value );
		redoUI();
	}

	public void setBrushWidth( float value ) {
		if( value < .1f ) value = .1f;
		if( value > 99f ) value = 99f;
		if( drawMode == DRAWMODE.BRUSH ) canvas.setCursorSize( penWidth );
		penWidth = value;
	}

	public void setEraserWidth( float value ) {
		if( value < .1f ) value = .1f;
		if( value > 99f ) value = 99f;
		if( drawMode == DRAWMODE.ERASER ) canvas.setCursorSize( value );
		eraserWidth = value;
	}

	public void setBackgroundColor( Color color ) {
		draw.setBackgroudColor( color );
		canvas.setBackground( color );
		canvas.repaint();
	}

	public void setSmoothing( boolean selected ) {
		lineSmoothing = selected;
	}

	public void setSmoothingDegree( int value ) {
		if( value < 0 ) value = 0;
		if( value > 9 ) value = 9;
		smoothingDegree = value;
	}

	public String getVersionString() {
		return VERSION_MAJOR + "." + VERSION_MINOR + "." + VERSION_BUILD;
	}
}
